<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>8.6.1-区分 vnode 的类型</title>
</head>

<body>
    <div id="app">
        <!-- <input value="foo" id="my-input" type="text" value="foo" class="foo" onchange="change()" /> -->
    </div>
</body>
    <script src="https://unpkg.com/@vue/reactivity@3.0.5/dist/reactivity.global.js"></script>
    <script>
        /**
         * 后续渲染时，为 render 函数传递了新的 vnode，则不会进行卸载操作，
         *  而是会把新旧 vnode 都传递给 patch 函数进行打补丁操作。
         * 
         * 具体执行打补丁操作之前，我们需要保证新旧 vnode 所描述的内容相同。
         * - 初次渲染的 vnode 是一个 p 元素， 后续又渲染了一个 input 元素
         * - p 元素和 input 元素之间不存在打补丁的意义，
         *    因为对于不同的元素来说，每个元素都有特有的属性 
         *    （input 元素有 type 属性，p 元素没有）
         * 
         * 正确的更新操作
         * - 在真正执行更新操作之前，我们优先检查新旧 vnode 所描述的内容是否相同，
         *    如果不同，则直接调用 unmount 函数将旧 vnode 卸载
         * - 卸载完成后，我们应该将参数 n1 的值重置为 null，
         *    这样才能保证后续挂载操作正确执行。
         * 
         * 即使新旧 vnode 描述的内容相同，我们仍然需要进一步确认它们的类型是否相同
         * - 一个 vnode 可以用来描述普通标签，
         *    也可以用来描述组件，还可以用来描述 Fragment 等
         * - 对于不同类型的 vnode，我们需要提供不同的挂载或打补丁的处理方式
         * */ 

        
         function createRenderer(options) {
            // 通过 options 得到操作 DOM 的 API
            const {
                createElement,
                insert,
                setElementText,
                patchProps
            } = options

            // 在这个作用域内定义的函数都可以访问那些 API

            function mountElement(vnode, container) {
                // 调用 createElement 函数创建元素
                // 让 vnode.el 引用真实 DOM 元素
                const el = vnode.el = createElement(vnode.type)
                
                if (typeof vnode.children === 'string') {
                    // 调用 setElementText 设置元素的文本节点
                    setElementText(el, vnode.children)
                }else if (Array.isArray(vnode.children)) {
                    // 如果 children 是数组，则遍历每一个子节点，并调用 patch 函数挂载它们
                    vnode.children.forEach(child => {
                        patch(null, child, el)
                    })
                }

                // 如果 vnode.props 存在才处理它
                if (vnode.props) {
                    // 遍历 vnode.props
                    for (const key in vnode.props) {
                        // 调用 patchProps 函数即可
                        patchProps(el, key, null, vnode.props[key])
                    }
                }

                // 调用 insert 函数将元素插入到容器内
                insert(el, container)
            }

            // 第一个参数 n1 代表旧 vnode，第二个参数 n2 代表新 vnode。
            function patch(n1, n2, container) {
                 // 如果 n1 存在，则对比 n1 和 n2 的类型
                 if (n1 && n1.type !== n2.type) { // 新增
                    // 如果新旧 vnode 的类型不同，则直接将旧 vnode 卸载
                    unmount(n1)
                    n1 = null
                }

                // 代码运行到这里，证明 n1 和 n2 所描述的内容相同
                const { type } = n2
                // 如果 n2.type 的值是字符串类型，则它描述的是普通标签元素
                if (typeof type === 'string') { // 新增 类型判断
                    // 如果 n1 不存在，意味着挂载，则调用 mountElement 函数完成挂载
                    if (!n1) {
                        mountElement(n2, container)
                    } else {
                        patchElement(n1, n2)
                    }
                } else if (typeof type === 'object') {
                    // 如果 n2.type 的值的类型是对象，则它描述的是组件
                } else if (type === 'xxx') {
                    // 处理其他类型的 vnode
                }
            }

            function render(vnode, container) {
                if (vnode) {
                    // 新 vnode 存在，将其与旧 vnode 一起传递给 patch 函数，进行打补丁
                    patch(container._vnode, vnode, container)
                } else {
                    if (container._vnode) {
                        // 旧 vnode 存在，且新 vnode 不存在，说明是卸载（unmount）操作
                        // 调用 unmount 函数卸载 vnode
                        unmount(container._vnode)
                    }
                }
                // 把 vnode 存储到 container._vnode 下，即后续渲染中的旧 vnode
                container._vnode = vnode
            }

            // 卸载操作
            function unmount(vnode) {
                const parent = vnode.el.parentNode
                if (parent) {
                    parent.removeChild(vnode.el)
                }
            }

            return {
                render
            }
        }

        // 创建一个渲染器
        // 在创建 renderer 时传入配置项
        const renderer = createRenderer({
            // 用于创建元素
            createElement(tag) {
                return document.createElement(tag)
            },
            // 用于设置元素的文本节点
            setElementText(el, text) {
                el.textContent = text
            },
            // 用于在给定的 parent 下添加指定元素
            insert(el, parent, anchor = null) {
                parent.insertBefore(el, anchor)
            },
            // 将属性设置相关操作封装到 patchProps 函数中，并作为渲染器选项传递
            patchProps(el, key, prevValue, nextValue) {
                // 对 class 进行特殊处理
                if (key === 'class') {
                    el.className = nextValue || ''
                } else if (shouldSetAsProps(el, key, nextValue)) {
                    // 获取该 DOM Properties 的类型
                    const type = typeof el[key]
                    // 如果是布尔类型，并且 value 是空字符串，则将值矫正为 true
                    if (type === 'boolean' && nextValue === '') {
                        el[key] = true
                    } else {
                        el[key] = nextValue
                    }
                } else {
                    el.setAttribute(key, nextValue)
                }

                // 判断是否只读，如果是，则使用 setAttribute 函数设置属性
                function shouldSetAsProps(el, key, value) {
                    // 特殊处理
                    if (key === 'form' && el.tagName === 'INPUT') return false
                    // 兜底
                    // 用 in 操作符判断 key 是否存在对应的 DOM Properties
                    return key in el
                }
            }
        })

        // 新增 测试
        const vnode = {
            type: 'input',
            props: {
                disabled: '',
                form: 'my-form',
                value: 'foo',
                class: 'foo bar baz'
            },
        }
        const vnode2 = {
            type: 'div',
            children: 'hello'
        }

        // 调用 render 函数渲染该 vnode
        renderer.render(vnode, document.querySelector('#app'))
        setTimeout(_=>{
            renderer.render(vnode2, document.querySelector('#app'))
        }, 1200)

    </script>
    

</html>